﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using log4net;
using System.IO;
using System.Web.Caching;

namespace Sherwood.SignOn.Client.UrlFilter
{

    /// <summary>
    /// Implementation of a url filtering engine which parses rules for allowing / denying
    /// access to urls based on user roles or username.
    /// </summary>
    public class UrlFilterEngine
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(UrlFilterEngine));

        private RuleConfig _filterRules = null;


        /// <summary>
        /// Container for a collection of FilterRules for the url engine.
        /// </summary>
        public class RuleConfig
        {
            public List<FilterRule> Rules { get; set; }
            public string DefaultDenyRedirectUrl { get; set; }
            public string DefaultAllowRedirectUrl { get; set; }

            public static RuleConfig FromXmlFile(string filePath)
            {
                RuleConfig filterRules = new RuleConfig();
                Rules xmlRules = Sherwood.SignOn.Client.UrlFilter.Rules.Load(filePath);
                filterRules.DefaultAllowRedirectUrl = xmlRules.defaultAllowRedirectUrl;
                filterRules.DefaultDenyRedirectUrl = xmlRules.defaultDenyRedirectUrl;
                filterRules.Rules = new List<FilterRule>();
                if (xmlRules.Rule != null && xmlRules.Rule.Count > 0)
                {
                    foreach (Rule xmlRule in xmlRules.Rule)
                    {
                        filterRules.Rules.Add(FilterRule.FromXmlRule(xmlRule));
                    }
                }
                return filterRules;
            }
        }


        /// <summary>
        /// Container for url filter rule
        /// </summary>
        public class FilterRule
        {
            public string AllowRedirectUrl { get; set; }
            public string DenyRedirectUrl { get; set; }
            public bool AlwaysAllow { get; set; }
            public bool AlwaysDeny { get; set; }
            public bool RequireSession { get; set; }
            public bool RequireNonPersistentSession { get; set; }
            public Regex Url { get; set; }
            public Regex Hosts { get; set; }
            public Regex Users { get; set; }
            public Regex Roles { get; set; }
            public Regex Referrer { get; set; }

            /// <summary>
            /// Creates a FilterRule object from a Rule read from an XML file.
            /// </summary>
            /// <param name="xmlRule"></param>
            /// <returns></returns>
            public static FilterRule FromXmlRule(Rule xmlRule)
            {
                return new FilterRule
                {
                    AllowRedirectUrl = xmlRule.allowRedirectUrl,
                    DenyRedirectUrl = xmlRule.denyRedirectUrl,
                    AlwaysAllow = xmlRule.alwaysAllow,
                    AlwaysDeny = xmlRule.alwaysDeny,
                    RequireSession = xmlRule.requireSession.GetValueOrDefault(true),
                    RequireNonPersistentSession = xmlRule.requireNonPersistentSession.GetValueOrDefault(false),
                    Url = CreateRegex(xmlRule.url),
                    Hosts = CreateRegex(xmlRule.hosts),
                    Users = CreateRegex(xmlRule.users),
                    Roles = CreateRegex(xmlRule.roles),
                    Referrer = CreateRegex(xmlRule.referrer)
                };                
            }           

            /// <summary>
            /// Helper method for creating regular expresion objects
            /// </summary>
            /// <param name="pattern"></param>
            /// <returns></returns>
            protected static Regex CreateRegex(string pattern)
            {
                if (!string.IsNullOrEmpty(pattern))
                {
                    return new Regex(pattern.Trim().ToLower(), RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
                }
                return null;
            }
        }



        /// <summary>
        /// List of rules to apply
        /// </summary>
        public RuleConfig LoadFilterRules()
        {
            string configurationFile = System.Configuration.ConfigurationManager.AppSettings["UrlFilterConfigurationFile"];
            string cacheKey = "URLFILTERRULESCACHE" + configurationFile;

            //Load rules from cache
            Cache cache = HttpContext.Current != null ? HttpContext.Current.Cache : null;
            if (cache != null)
            {
                var cachedRules = cache[cacheKey] as RuleConfig;
                if (cachedRules != null)
                {
                    if (Log.IsDebugEnabled)
                        Log.Debug(cachedRules.Rules.Count + " rules loaded from cache.");
                    return cachedRules;
                }
            }
            if (!File.Exists(configurationFile))
            {
                configurationFile = HttpContext.Current.Server.MapPath(configurationFile);
            }

            if (!File.Exists(configurationFile))
            {
                throw new Exception("Configuration file for url filter does not exist at the specified path.");
            }

            var rules = RuleConfig.FromXmlFile(configurationFile);

            if (cache != null)
            {
                //Create cache dependency
                var fileDependency = new CacheDependency(configurationFile);
                var expiration = TimeSpan.FromDays(7);
                cache.Add(cacheKey, rules, fileDependency, Cache.NoAbsoluteExpiration, expiration,
                          CacheItemPriority.High, null);
            }
            if (Log.IsDebugEnabled)
                Log.Debug(rules.Rules.Count + " rules loaded from file.");
            return rules;
        }

        public RuleConfig FilterRules
        {
            get { return _filterRules ?? (_filterRules = LoadFilterRules()); }
        }


        /// <summary>
        /// Checks rules to determine where or not access should be granted to
        /// the specified url.
        /// </summary>
        /// <param name="url">Url to gain access to.</param>
        /// <param name="username"></param>
        /// <param name="host"></param>
        /// <param name="roles"></param>
        /// <param name="referrer"></param>
        /// <returns>True if user has acces</returns>
        public bool CheckAccess(string url, string username, string host, string[] roles, string referrer)
        {
            string redirectUrl;
            bool requireSession;
            bool requireNonPersistentSession;
            return CheckAccess(url, username, host, roles, referrer, out redirectUrl, out requireSession, out requireNonPersistentSession);
        }

        /// <summary>
        /// Checks rules to determine where or not access should be granted to
        /// the specified url.
        /// </summary>
        /// <param name="url">Url to gain access to.</param>
        /// <param name="username"></param>
        /// <param name="roles"></param>
        /// <returns>True if user has acces</returns>
        public bool CheckAccess(string url, string username, string host, string[] roles, string referrer, out string redirectUrl, out bool requireNonPersistentSession)
        {
            bool requireSession;
            return CheckAccess(url, username, host, roles, referrer, out redirectUrl, out requireSession, out requireNonPersistentSession);
        }



        /// <summary>
        /// Checks rules to determine where or not access should be granted to
        /// the specified url.
        /// </summary>
        /// <param name="url">Url to gain access to.</param>
        /// <param name="username"></param>
        /// <param name="roles"></param>
        /// <param name="redirectUrl">URL to redirect user to. Value is null if user does not need to be redirected.
        /// <param name="requireSession">Indicates whether the client should retreive a session from the sign on server, even if the user has annonymous access.
        /// A session is required if a rule that matches the Url is defined and the user (annonymous or not) has access. If no rule is defined, it is assumed that no session is required.</param>
        /// Redirect URL is applicable both in cases where function returns true or false.
        /// </param>
        /// <returns>True if user has access</returns>
        public bool CheckAccess(string url, string username, string host, string[] roles, string referrer, out string redirectUrl, out bool requireSession, out bool requireNonPersistentSession)
        {
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException("url");

            Log.Debug("Starting to check access for " + username + ". URL: " + url);
            bool hasAccess = true;
            requireSession = false;
            requireNonPersistentSession = false;
            redirectUrl = null;
            username = username ?? "";
            
            foreach (FilterRule rule in FilterRules.Rules)
            {
                if (rule.Url == null) continue;
                Log.Debug("Testing rule with url pattern: " + rule.Url);
                //If rule matches URL
                if (!rule.Url.IsMatch(url))
                {
                    Log.Debug("Current url does not match: " + rule.Url);
                }
                else
                {
                    Log.Debug("Current url matches: " + rule.Url);

                    //Does this rule require a non-persistent session?
                    requireNonPersistentSession = rule.RequireNonPersistentSession;

                    //Check for username match
                    bool usernameMatch = false;
                    bool usernameConditionExists = rule.Users != null;
                    if (usernameConditionExists)
                    {
                        usernameMatch = rule.Users.IsMatch(username);
                    }

                    //Check for host match
                    bool hostMatch = false;
                    bool hostConditionExists = rule.Hosts != null;
                    if (hostConditionExists)
                    {
                        hostMatch = rule.Hosts.IsMatch(host);
                    }

                    //Check for role match
                    bool roleMatch = false;
                    bool roleConditionExists = rule.Roles != null;
                    if (roleConditionExists)
                    {
                        if (roles == null || (roles != null && roles.Length == 0))
                        {
                            roleMatch = rule.Roles.IsMatch("");
                            //regular expressions that match an empty string should logically match a null roles collection.
                        }
                        else
                        {
                            foreach (string role in roles)
                            {
                                roleMatch = role != null && rule.Roles.IsMatch(role.Trim());
                                if (roleMatch)
                                    break;
                            }
                        }
                    }

                    //Check for referrer match
                    bool referrerMatch = false;
                    bool referrerConditionExists = rule.Referrer != null;
                    if (referrerConditionExists)
                    {
                        referrerMatch = rule.Referrer.IsMatch(referrer);
                    }

                    var hasConditions = hostConditionExists || usernameConditionExists ||
                                    roleConditionExists || referrerConditionExists;
                    var ruleIsMatch = (!hostConditionExists || hostMatch) && (!usernameConditionExists || usernameMatch) &&
                                      (!roleConditionExists || roleMatch) && (!referrerConditionExists || referrerMatch);
                    if (rule.AlwaysDeny)
                    {
                        //A condition must exist for an always deny rule to be applied
                        if (hasConditions && ruleIsMatch)
                        {
                            redirectUrl = !string.IsNullOrEmpty(rule.DenyRedirectUrl)
                                              ? rule.DenyRedirectUrl.Trim()
                                              : null;
                            if (string.IsNullOrEmpty(redirectUrl) &&
                                !string.IsNullOrEmpty(FilterRules.DefaultDenyRedirectUrl))
                            {
                                redirectUrl = FilterRules.DefaultDenyRedirectUrl;
                            }
                            if (Log.IsDebugEnabled)
                            {
                                Log.Debug(username + " denied access to " + url +
                                          " based on 'alwaysDeny' rule: " + rule.Url + ", " +
                                          "rule users: " + rule.Users + ", " +
                                          "rule roles: " + rule.Roles + ", " +
                                          "rule hosts: " + rule.Hosts + ", " +
                                          "rule referrer: " + rule.Referrer);
                            }
                            return false; //Deny access
                        }
                    }
                    else if (rule.AlwaysAllow)
                    {
                        //A condition must exist for an always allow rule to be applied
                        if (hasConditions && ruleIsMatch)
                        {
                            redirectUrl = !string.IsNullOrEmpty(rule.AllowRedirectUrl)
                                              ? rule.AllowRedirectUrl.Trim()
                                              : null;
                            if (string.IsNullOrEmpty(redirectUrl) &&
                                !string.IsNullOrEmpty(FilterRules.DefaultAllowRedirectUrl))
                            {
                                redirectUrl = FilterRules.DefaultAllowRedirectUrl;
                            }
                            if (Log.IsDebugEnabled)
                            {
                                Log.Debug(username + " granted access to " + url +
                                          " based on 'alwaysAllow' rule: " + rule.Url + ", " +
                                          "rule users: " + rule.Users + ", " +
                                          "rule roles: " + rule.Roles + ", " +
                                          "rule hosts: " + rule.Hosts + ", " +
                                          "rule referrer: " + rule.Referrer);
                            }
                            requireSession = rule.RequireSession;
                            return true; //Allow access
                        }
                    }
                    else
                    {
                        //Set hasAccess flag based on the current rule
                        //This could be overridden by any following rules.
                        hasAccess = ruleIsMatch;
                        if (hasAccess)
                        {
                            requireSession = rule.RequireSession;
                            if (Log.IsDebugEnabled)
                            {
                                Log.Debug(username + " would granted access to " + url +
                                    " based on rule: " + rule.Url + ", " +
                                          "rule users: " + rule.Users + ", " +
                                          "rule roles: " + rule.Roles + ", " +
                                          "rule hosts: " + rule.Hosts + ", " +
                                          "rule referrer: " + rule.Referrer);
                            }
                            redirectUrl = !string.IsNullOrEmpty(rule.AllowRedirectUrl)
                                              ? rule.AllowRedirectUrl.Trim()
                                              : null;
                            if (string.IsNullOrEmpty(redirectUrl) &&
                                !string.IsNullOrEmpty(FilterRules.DefaultAllowRedirectUrl))
                            {
                                redirectUrl = FilterRules.DefaultAllowRedirectUrl;
                            }
                        }
                        else
                        {
                            Log.Debug(username + " would be denied access to " + url + " based on rule: " +
                                      (rule.Url != null ? rule.Url.ToString() : "") + ", rule users: " +
                                      (rule.Users != null ? rule.Users.ToString() : "") + ", rule roles: " +
                                      (rule.Roles != null ? rule.Roles.ToString() : "") + ", rule hosts: " +
                                      (rule.Hosts != null ? rule.Hosts.ToString() : ""));
                            redirectUrl = !string.IsNullOrEmpty(rule.DenyRedirectUrl)
                                              ? rule.DenyRedirectUrl.Trim()
                                              : null;
                            if (string.IsNullOrEmpty(redirectUrl) &&
                                !string.IsNullOrEmpty(FilterRules.DefaultDenyRedirectUrl))
                            {
                                redirectUrl = FilterRules.DefaultDenyRedirectUrl;
                            }
                        }
                    }
                }
            }
            if (Log.IsDebugEnabled)
            {
                if (hasAccess)
                    Log.Debug(username + " granted access to " + url + " after applying final rule.");
                else
                    Log.Debug(username + " denied access to " + url + " after applying final rule.");
            }
            return hasAccess;
        }

    }
}
